/*  SPDX-License-Identifier: GPL-2.0-or-later */
/*!********************************************************************

  Audacity: A Digital Audio Editor

  ImportMP3_MPG123.cpp

  Dmitry Vedenko

*/

#include <wx/defs.h>
#include <cstddef>
#include <cstring>

#include "libraries/lib-import-export/Import.h"
#include "libraries/lib-import-export/ImportUtils.h"
#include "libraries/lib-import-export/ImportProgressListener.h"
#include "Project.h"

#define DESC XO("MP3 files")

#include <wx/string.h>
#include <wx/log.h>

#include <mpg123.h>

#include "libraries/lib-tags/Tags.h"
#include "WaveTrack.h"

#include "CodeConversions.h"
#include "FromChars.h"

#include "ImportMP3_MPG123.h"

namespace {
// ID2V2 genre can be quite complex:
// (from https://id3.org/id3v2.3.0)
// References to the ID3v1 genres can be made by, as first byte, enter
// "(" followed by a number from the genres list (appendix A) and ended
// with a ")" character. This is optionally followed by a refinement,
// e.g. "(21)" or "(4)Eurodisco". Several references can be made in the
// same frame, e.g. "(51)(39)". However, Audacity only supports one
// genre, so we just skip ( a parse the number afterwards.
wxString GetId3v2Genre(Tags& tags, const char* genre)
{
    if (genre == nullptr) {
        return {};
    }

    // It was observed, however, that Genre can use a different format
    if (genre[0] != '(') {
        // We consider the string to be a genre name
        return audacity::ToWXString(genre);
    }

    auto it = genre;
    auto end = it + std::strlen(it);

    while (*it == '(')
    {
        int tagValue;
        auto result = FromChars(++it, end, tagValue);

        // Parsing failed, consider it to be the genre
        if (result.ec != std::errc {}) {
            break;
        }

        const auto parsedGenre = tags.GetGenre(tagValue);

        if (!parsedGenre.empty()) {
            return parsedGenre;
        }

        it = result.ptr;

        // Nothing left to parse
        if (it == end) {
            break;
        }

        // Unexpected symbol in the tag
        if (*it != ')') {
            break;
        }

        ++it;
    }

    if (it != end) {
        return audacity::ToWXString(it);
    }

    return audacity::ToWXString(genre);
}
}

MP3ImportPlugin::MP3ImportPlugin()
    : ImportPlugin(
        FileExtensions(exts.begin(), exts.end()))
{
#if MPG123_API_VERSION < 46
    // Newer versions of the library don't need that anymore, but it is safe
    // to have the no-op call present for compatibility with old versions.
    mpg123_init();
#endif
}

wxString MP3ImportPlugin::GetPluginStringID()
{
    return wxT("libmpg123");
}

TranslatableString MP3ImportPlugin::GetPluginFormatDescription()
{
    return DESC;
}

std::unique_ptr<ImportFileHandle> MP3ImportPlugin::Open(
    const FilePath& Filename, AudacityProject*)
{
    auto handle = std::make_unique<MP3ImportFileHandle>(Filename);

    if (!handle->Open()) {
        return nullptr;
    }

    return handle;
}

// ============================================================================
// MP3ImportFileHandle
// ============================================================================

MP3ImportFileHandle::MP3ImportFileHandle(const FilePath& filename)
    : ImportFileHandleEx(filename)
{
    int errorCode = MPG123_OK;
    mHandle = mpg123_new(nullptr, &errorCode);

    if (errorCode != MPG123_OK) {
        wxLogError(
            "Failed to create MPG123 handle: %s",
            mpg123_plain_strerror(errorCode));

        mHandle = nullptr;

        return;
    }

    errorCode = mpg123_replace_reader_handle(
        mHandle, ReadCallback, SeekCallback, nullptr);

    if (errorCode != MPG123_OK) {
        wxLogError(
            "Failed to set reader on the MPG123 handle: %s",
            mpg123_plain_strerror(errorCode));

        mpg123_delete(mHandle);
        mHandle = nullptr;
    }

    // We force mpg123 to decode into floats
    mpg123_param(mHandle, MPG123_FLAGS, MPG123_GAPLESS | MPG123_FORCE_FLOAT, 0.0);

    if (errorCode != MPG123_OK) {
        wxLogError(
            "Failed to set options on the MPG123 handle",
            mpg123_plain_strerror(errorCode));

        mpg123_delete(mHandle);
        mHandle = nullptr;
    }
}

MP3ImportFileHandle::~MP3ImportFileHandle()
{
    // nullptr is a valid input for the mpg123_delete
    mpg123_delete(mHandle);
}

TranslatableString MP3ImportFileHandle::GetFileDescription()
{
    return DESC;
}

auto MP3ImportFileHandle::GetFileUncompressedBytes() -> ByteCount
{
    // We have to parse the file first using mpg123_scan,
    // we do not want to do that before the import starts.
    return 0;
}

wxInt32 MP3ImportFileHandle::GetStreamCount()
{
    return 1;
}

const TranslatableStrings& MP3ImportFileHandle::GetStreamInfo()
{
    static TranslatableStrings empty;
    return empty;
}

void MP3ImportFileHandle::SetStreamUsage(wxInt32 WXUNUSED(StreamID), bool WXUNUSED(Use))
{
}

void MP3ImportFileHandle::Import(
    ImportProgressListener& progressListener, WaveTrackFactory* trackFactory,
    TrackHolders& outTracks, Tags* tags,
    std::optional<LibFileFormats::AcidizerTags>&)
{
    BeginImport();

    auto finalAction = finally([handle = mHandle]() { mpg123_close(handle); });

    outTracks.clear();
    mTrackFactory = trackFactory;

    long long framesCount = mpg123_framelength(mHandle);

    if (!SetupOutputFormat()) {
        progressListener.OnImportResult(ImportProgressListener::ImportResult::Error);
        return;
    }

    off_t frameIndex { 0 };
    unsigned char* data { nullptr };
    size_t dataSize { 0 };

    std::vector<float> conversionBuffer;

    int ret = MPG123_OK;

    while ((ret = mpg123_decode_frame(mHandle, &frameIndex, &data, &dataSize))
           == MPG123_OK)
    {
        if (framesCount > 0) {
            progressListener.OnImportProgress(static_cast<double>(frameIndex) / static_cast<double>(framesCount));
        }

        if (IsCancelled()) {
            progressListener.OnImportResult(ImportProgressListener::ImportResult::Cancelled);
            return;
        }
        //VS: doesn't implement Stop behavior...

        constSamplePtr samples = reinterpret_cast<constSamplePtr>(data);
        const size_t samplesCount = dataSize / sizeof(float) / mNumChannels;

        // libmpg123 picks up the format based on some "internal" precision.
        // This case is not expected to happen
        if (mFloat64Output) {
            conversionBuffer.resize(samplesCount * mNumChannels);

            for (size_t sampleIndex = 0; sampleIndex < conversionBuffer.size();
                 ++sampleIndex) {
                conversionBuffer[sampleIndex] = static_cast<float>(
                    reinterpret_cast<const double*>(data)[sampleIndex]);
            }

            samples = reinterpret_cast<constSamplePtr>(conversionBuffer.data());
        }
        // Just copy the interleaved data to the channels
        unsigned chn = 0;
        ImportUtils::ForEachChannel(*mTrack, [&](auto& channel)
        {
            channel.AppendBuffer(
                samples + sizeof(float) * chn,
                floatSample, samplesCount,
                mNumChannels,
                floatSample);
            ++chn;
        });
    }

    if (ret != MPG123_DONE) {
        wxLogError(
            "Failed to decode MP3 file: %s", mpg123_plain_strerror(ret));

        progressListener.OnImportResult(ImportProgressListener::ImportResult::Error);
        return;
    }

    ImportUtils::FinalizeImport(outTracks, *mTrack);

    ReadTags(tags);

    progressListener.OnImportResult(ImportProgressListener::ImportResult::Success);
}

bool MP3ImportFileHandle::SetupOutputFormat()
{
    long rate;
    int channels;
    int encoding = MPG123_ENC_FLOAT_32;
    mpg123_getformat(mHandle, &rate, &channels, &encoding);

    mNumChannels = channels == MPG123_MONO ? 1 : 2;

    if (encoding != MPG123_ENC_FLOAT_32 && encoding != MPG123_ENC_FLOAT_64) {
        wxLogError("MPG123 returned unexpected encoding");

        return false;
    }

    mFloat64Output = encoding == MPG123_ENC_FLOAT_64;

    mTrack = ImportUtils::NewWaveTrack(
        *mTrackFactory,
        mNumChannels,
        floatSample,
        rate);

    return true;
}

void MP3ImportFileHandle::ReadTags(Tags* tags)
{
    mpg123_id3v1* v1;
    mpg123_id3v2* v2;
    int meta;

    meta = mpg123_meta_check(mHandle);

    if (meta & MPG123_ID3 && mpg123_id3(mHandle, &v1, &v2) == MPG123_OK) {
        if (v2 != nullptr && v2->title != nullptr && v2->title->fill > 0) {
            tags->SetTag(TAG_TITLE, audacity::ToWXString(v2->title->p));
        } else if (v1 != nullptr && v1->title[0] != '\0') {
            tags->SetTag(TAG_TITLE, audacity::ToWXString(v1->title));
        }

        if (v2 != nullptr && v2->artist != nullptr && v2->artist->fill > 0) {
            tags->SetTag(TAG_ARTIST, audacity::ToWXString(v2->artist->p));
        } else if (v1 != nullptr && v1->artist[0] != '\0') {
            tags->SetTag(TAG_ARTIST, audacity::ToWXString(v1->artist));
        }

        if (v2 != nullptr && v2->album != nullptr && v2->album->fill > 0) {
            tags->SetTag(TAG_ALBUM, audacity::ToWXString(v2->album->p));
        } else if (v1 != nullptr && v1->album[0] != '\0') {
            tags->SetTag(TAG_ALBUM, audacity::ToWXString(v1->album));
        }

        if (v2 != nullptr && v2->year != nullptr && v2->year->fill > 0) {
            tags->SetTag(TAG_YEAR, audacity::ToWXString(v2->year->p));
        } else if (v1 != nullptr && v1->year[0] != '\0') {
            tags->SetTag(TAG_YEAR, audacity::ToWXString(std::string(v1->year, 4)));
        }

        if (v2 != nullptr && v2->genre != nullptr && v2->genre->fill > 0) {
            tags->SetTag(TAG_GENRE, GetId3v2Genre(*tags, v2->genre->p));
        } else if (v1 != nullptr) {
            tags->SetTag(TAG_GENRE, tags->GetGenre(v1->genre));
        }

        if (v2 != nullptr && v2->comment != nullptr && v2->comment->fill > 0) {
            tags->SetTag(TAG_COMMENTS, audacity::ToWXString(v2->comment->p));
        } else if (v1 != nullptr && v1->comment[0] != '\0') {
            tags->SetTag(TAG_COMMENTS, audacity::ToWXString(v1->comment));
        }

        if (v2 != nullptr) {
            for (size_t i = 0; i < v2->comments; ++i) {
                if (v2->comment_list[i].text.fill == 0) {
                    continue;
                }

                tags->SetTag(
                    audacity::ToWXString(std::string(v2->comment_list[i].id, 4)),
                    audacity::ToWXString(v2->comment_list[i].text.p));
            }

            for (size_t i = 0; i < v2->extras; ++i) {
                if (v2->extra[i].text.fill == 0) {
                    continue;
                }

                tags->SetTag(
                    audacity::ToWXString(std::string(v2->extra[i].id, 4)),
                    audacity::ToWXString(v2->extra[i].text.p));
            }

            // libmpg123 does not parse TRCK tag, we have to do it ourselves
            for (size_t i = 0; i < v2->texts; ++i) {
                if (memcmp(v2->text[i].id, "TRCK", 4) == 0) {
                    tags->SetTag(
                        TAG_TRACK, audacity::ToWXString(v2->text[i].text.p));
                }
            }
        }
    }
}

bool MP3ImportFileHandle::Open()
{
    if (mHandle == nullptr) {
        return false;
    }

    // Open the file
    if (!mFile.Open(GetFilename())) {
        return false;
    }

    // Get the length of the file
    mFileLen = mFile.Seek(0, wxFromEnd);

    if (mFileLen == wxInvalidOffset || mFile.Error()) {
        mFile.Close();
        return false;
    }

    if (mFile.Seek(0, wxFromStart) == wxInvalidOffset || mFile.Error()) {
        mFile.Close();
        return false;
    }

    // Check if file is an MP3
    auto errorCode = mpg123_open_handle(mHandle, this);

    if (errorCode != MPG123_OK) {
        return false;
    }

    // Scan the file
    errorCode = mpg123_scan(mHandle);

    if (errorCode != MPG123_OK) {
        return false;
    }

    // Read the output format
    errorCode = mpg123_decode_frame(mHandle, nullptr, nullptr, nullptr);

    // First decode should read the format
    if (errorCode != MPG123_NEW_FORMAT) {
        return false;
    }

    return true;
}

ptrdiff_t MP3ImportFileHandle::ReadCallback(
    void* handle, void* buffer, size_t size)
{
    return static_cast<MP3ImportFileHandle*>(handle)->mFile.Read(buffer, size);
}

wxSeekMode GetWXSeekMode(int whence)
{
    switch (whence) {
    case SEEK_SET:
        return wxFromStart;
    case SEEK_CUR:
        return wxFromCurrent;
    case SEEK_END:
        return wxFromEnd;
    default:
        // We have covered all the lseek whence modes defined by POSIX
        // so this branch should not be reachable
        assert(false);
        return wxFromStart;
    }
}

off_t MP3ImportFileHandle::SeekCallback(
    void* handle, off_t offset, int whence)
{
    return static_cast<MP3ImportFileHandle*>(handle)->mFile.Seek(
        offset, GetWXSeekMode(whence));
}
